feat(questionnaires): F8.2 result exports#54
Merged
Conversation
Add the record-level read-side companion to F8.1's aggregate analytics: download a version's completed-session results as CSV (one row per session × question) or JSON (the full session graph — answers + provenance + turns). A single admin-only GET route serves both via ?format=, reusing the F8.1 analytics filter (date window + tags) so an export mirrors exactly what the admin is viewing. - Export library (pure serialisers, Prisma at the seam) in lib/app/questionnaire/export: results-types, results-loader (loadResultsExport — batched cousin of the single-session PDF loader; MAX_EXPORT_SESSIONS=5000 cap → capped), results-serialize (csvEscape + renderAnswerValue / bare JSON), results-query (analytics filter + format). - Route .../versions/[vid]/export (master-flag-gated, version-scoped, exportLimiter bulk-read sub-cap) + versionExport endpoint builder. - Analytics-page export buttons (export-buttons.tsx) carrying the page's current filter; blob download via Content-Disposition. Anonymous mode (AppQuestionnaireConfig.anonymousMode) is honoured at the data boundary: respondent identity is nulled AND every session's turns array is dropped (raw respondent prose never leaves the server). Answer values are always present — anonymity is non-linkage, not redaction. Completed sessions only. Tests: serialiser + loader unit (anonymous redaction, completed-only window, batched name resolution, turn-ordinal mapping, cap, branches), export-buttons component, route integration (auth/flag/scope/format headers/429), versionExport endpoint assertion. Docs + F8.2 tracker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds F8.2 — result exports, the record-level read-side companion to F8.1's aggregate analytics. An admin can download a version's completed-session results from the analytics page as:
csvEscape).A single admin-only
GET .../versions/[vid]/export?format=csv|jsonserves both, reusing the F8.1 analytics filter (date window + tags) so an export mirrors exactly what the admin is viewing.Anonymous-mode contract
When the version's
AppQuestionnaireConfig.anonymousMode = true, the loader (results-loader.ts) honours it at the data boundary, not just the UI:turnsarray is dropped (raw respondent prose never leaves the server — turns are still loaded so answer→turn-ordinal math works, but they're never serialised out).Answer values are always present in both formats — anonymity is about non-linkage, not redacting the survey data (mirrors the F7.4 PDF export). Full cross-surface anonymous audit is deferred to F8.3.
Decisions (confirmed with the user)
completed,createdAtin window). Status is still a column/field.?format=csv|json(defaultjson), mirroring the orchestration conversation-export route.csvEscape, the single-session PDF loader's anonymous stance, and theexportLimiterbulk-read sub-cap. No migration.Changes
lib/app/questionnaire/export/{results-types,results-loader,results-serialize,results-query}.ts.loadResultsExportbatches the single-session PDF loader; one batcheduser.findManyfor names (no N+1);MAX_EXPORT_SESSIONS = 5000cap →capped.app/api/v1/app/questionnaires/[id]/versions/[vid]/export/route.ts(master-flag-gated, version-scoped,exportLimitersub-cap) +API.APP.QUESTIONNAIRES.versionExport(id, vid).components/admin/questionnaires/analytics/export-buttons.tsx, wired into the analytics page beside the version selector, carrying the page's current filter..context/admin/questionnaire-analytics.md;.context/app/planning/features/f8.2.mdtracker.Testing
renderAnswerValue, JSON fidelity), loader (completed-only window, batched name resolution only when not anonymous, anonymous nulls identity + drops turns without querying users, turn-ordinal mapping, cap, tag filter, null/fallback branches), query schema,export-buttonscomponent (URL build, 429/error, blob download).?format=csvcontent-type +attachmentfilename +no-store/ resolved scope reaches loader /exportLimiter429.npm run validateclean. Changed files all ≥80% coverage.🤖 Generated with Claude Code